using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics.Contracts;
using System.Linq;
using System.Reflection;

namespace Charisma.Metadata
{
    public class MetaDataProvider : IMetaDataProvider
    {
        private static readonly ConcurrentDictionary<Type, object> types;

        private static readonly ReadOnlyCollection<string> emptyConstructorArguments =
            new ReadOnlyCollection<string>(new List<string>());

        static MetaDataProvider()
        {
            types = new ConcurrentDictionary<Type, object>(4, 8);
        }

        #region IMetaDataProvider Members

        public MetaType GetMetaType<T>()
        {
            return GetMetaType(typeof (T));
        }

        public virtual MetaType GetMetaType(Type type)
        {
            return (MetaType) types.GetOrAdd(type, BuildMetaType);
        }

        #endregion

        private static MetaType BuildMetaType(Type type)
        {
            Contract.Requires(type != null);
            Contract.Requires(!type.IsGenericTypeDefinition);
            Contract.Requires(!type.IsGenericParameter);
            Contract.Ensures(Contract.Result<MetaType>() != null);

            Dictionary<string, MetaFieldDescription> fields = type.GetProperties(BindingFlags.Public |
                                                                                 BindingFlags.Instance)
                .Cast<MemberInfo>()
                .Concat(type.GetFields(BindingFlags.Public | BindingFlags.Instance))
                .ToDictionary(
                    mi => mi.Name.ToLowerInvariant(),
                    mi => new MetaFieldDescription(mi.Name, mi.Name, mi));
            return BuildMetaTypeInternal(
                type,
                fields,
                null,
                s => s == "$id" || s == "_id",
                FilterFields);
        }

        private static bool FilterFields(MemberInfo mi)
        {
            var pi = mi as PropertyInfo;
            if (pi != null)
                return pi.GetGetMethod() != null && pi.GetSetMethod(true) != null && pi.GetIndexParameters().Length == 0;
            return true;
        }

        internal static MetaType BuildMetaTypeInternal(
            Type type,
            Dictionary<string, MetaFieldDescription> fields,
            Func<object, object> materializationHandler,
            Func<string, bool> isKey,
            Func<MemberInfo, bool> filter)
        {
            Contract.Requires(type != null);
            Contract.Requires(!type.IsGenericTypeDefinition);
            Contract.Requires(!type.IsGenericParameter);
            Contract.Requires(fields != null);
            Contract.Requires(isKey != null);
            Contract.Requires(filter != null);

            if (type.IsArray)
                return new MetaType(type, ReflectionService.BuildFactoryMethod(type, null), materializationHandler);

            bool parameterizedCtor;
            ConstructorInfo ctorInfo = GetConstructor(type, out parameterizedCtor);
            Func<object[], object> factoryMethod = ReflectionService.BuildFactoryMethod(type, ctorInfo);
            var metaProperties = new List<MetaField>();
            var result = new MetaType(type, factoryMethod, materializationHandler);

            if (parameterizedCtor)
            {
                ParameterInfo[] parameters = ctorInfo.GetParameters();
                var constructorArguments = new List<string>();
                foreach (ParameterInfo parameter in parameters)
                {
                    string key = parameter.Name.ToLowerInvariant();
                    MetaFieldDescription description;
                    if (!fields.TryGetValue(key, out description))
                        throw new InvalidOperationException(
                            string.Format(
                                "There is no property or field that matches constructor argument '{0} {1}' declared in type '{2}'",
                                parameter.ParameterType, parameter.Name, type.Name));
                    var property = description.Field as PropertyInfo;
                    if (property != null)
                    {
                        if (property.PropertyType != parameter.ParameterType)
                            throw new InvalidOperationException(
                                string.Format(
                                    "Type of property '{0} {1}' does not match type of constructor argument '{2} {3}' declared in type '{4}'",
                                    property.PropertyType, description.Name, parameter.ParameterType, parameter.Name,
                                    type.Name));
                        if (property.GetGetMethod() == null)
                            throw new InvalidOperationException(
                                string.Format("Property '{0} {1}' does not have public getter declared in type '{2}'.",
                                              property.PropertyType, description.Name, type.Name));
                        if (property.GetIndexParameters().Length > 0)
                            throw new InvalidOperationException(
                                string.Format(
                                    "Indexed properties are not supported. Property '{0} {1}' declared in type '{2}'",
                                    property.PropertyType, description.Name, type.Name));
                    }
                    var metaField = new MetaField(result, description);
                    metaProperties.Add(metaField);
                    if (isKey(metaField.Name))
                        result.KeyField = metaField;
                    fields.Remove(key);
                    constructorArguments.Add(metaField.Name);
                }
                result.ConstructorArguments = constructorArguments.AsReadOnly();
            }
            else
                result.ConstructorArguments = emptyConstructorArguments;

            foreach (MetaFieldDescription description in fields.Values)
            {
                if (!filter(description.Field))
                    continue;
                var metaProperty = new MetaField(result, description);
                metaProperties.Add(metaProperty);
                if (isKey(metaProperty.Name))
                    result.KeyField = metaProperty;
            }

            foreach (MetaField metaProperty in metaProperties)
                result.Fields.Add(metaProperty);

            return result;
        }

        private static ConstructorInfo GetConstructor(Type type, out bool parameterizedCtor)
        {
            Contract.Requires(type != null);
            Contract.Requires(!type.IsInterface);
            Contract.Requires(!type.IsAbstract);
            Contract.Requires(!type.IsGenericTypeDefinition);
            Contract.Requires(!type.IsGenericParameter);
            Contract.Ensures(Contract.Result<ConstructorInfo>() != null);

            parameterizedCtor = false;
            ConstructorInfo ctorInfo = type.GetConstructor(
                BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.CreateInstance | BindingFlags.Instance,
                null,
                Type.EmptyTypes,
                null);
            if (ctorInfo == null)
            {
                ConstructorInfo[] constructors =
                    type.GetConstructors(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.CreateInstance |
                                         BindingFlags.Instance);
                if (constructors.Length != 1)
                    throw new MissingMemberException(
                        string.Format(
                            "Type '{0}' should have parameterless constructor or only one constructor with arguments.",
                            type));
                ctorInfo = constructors[0];
                parameterizedCtor = true;
            }
            return ctorInfo;
        }
    }
}